package jspeech;

import java.io.IOException;
import jXMLReader.XMLReader;
import jspeech.exception.*;
import jspeech.sys.LinkLayer;
import jspeech.exception.XMLConfigFileException;

/**
 * Provides access to the speech speak service (TTS).
 * <p>
 * &nbsp;
 * <p>
 * <b>Workflow:</b>
 * <p>
 * <table>
 * <tr>
 * <td>Step 1:</td>
 * <td>Create <code>SpeechSpeaker</code></td>
 * </tr>
 * <td>Step 2:</td>
 * <td>Start the SpeechSpeaker by using <code>start()</code></td>
 * </tr>
 * <tr>
 * <td>&nbsp;Exception:</td>
 * <td><code>throw new SDKConnectionException</code></td>
 * </tr>
 * <tr>
 * <td>&nbsp;</td>
 * <td><code>throw new SDKNotFoundException</code></td>
 * </tr>
 * <tr>
 * <td>&nbsp;</td>
 * <td><code>throw new TimeoutException</code></td>
 * </tr>
 * <tr>
 * <td>Step 4:</td>
 * <td>Use <code>TTS(String)</code> to triger the compter to speak</td>
 * </tr>
 * <tr>
 * <td>Step 5:</td>
 * <td>Close the SpeechSpeaker by using <code>close()</code></td>
 * </tr>
 * </table>
 * <p>
 * &nbsp;
 * <p>
 * <b>Workflow when running <code>start()</code>:</b>
 * <p>
 * <table>
 * <tr>
 * <td>Step 1:</td>
 * <td>Create <code>LinkLayer</code> object. </td>
 * </tr>
 * <tr>
 * <td>Step 2:</td>
 * <td>Start establish the connection by using <code>LinkLayer.start()</code></td>
 * </tr>
 * <tr>
 * <td>&nbsp;Exception:</td>
 * <td><code>throw new SDKConnectionException</code></td>
 * </tr>
 * <tr>
 * <td>&nbsp;</td>
 * <td><code>throw new SDKNotFoundException</code></td>
 * </tr>
 * <tr>
 * <td>&nbsp;</td>
 * <td><code>throw new TimeoutException</code></td>
 * </tr>
 * </table>
 * <p>
 * 
 * &nbsp;
 * 
 * @author Kenneth Lo
 */
public class SpeechSpeaker {

	/**
	 * VOICE_MARY (Only available in Windows XP)
	 */
	public int VOICE_MARY;

	/**
	 * VOICE_MIKE (Only available in Windows XP)
	 */
	public int VOICE_MIKE;

	/**
	 * VOICE_SAM (Only available in Windows XP)
	 */
	public int VOICE_SAM;

	/**
	 * VOICE_SimplifiedChinese
	 */
	public int VOICE_SimplifiedChinese;

	/**
	 * VOICE_SAM (Only available in Windows Vista)
	 */
	public int VOICE_ANNA;

	/**
	 * Timeout period (in ms)
	 */
	public int TIMEOUT;

	/**
	 * Full path of the Speech Speech Engine
	 */
	public String SDK_PATH;

	/**
	 * If this set to true, the engine will load from the path set from the
	 * config file, otherwise, the engines should start manually.
	 */
	public boolean Load_Engine_From_Config;

	/**
	 * If the debug mode is set to true, detail flow message will print out to
	 * the console.
	 */
	public boolean Debug_Mode;

	/**
	 * The communication layer for sending and revicing data between the
	 * <i>JSpeech</i> and <i>TTS Engine</i>
	 */
	private LinkLayer linkLayer;

	/**
	 * Create the SpeechSpeaker object, bound to the default port (56000).
	 * 
	 */
	public SpeechSpeaker(String xmlConfigPath) {
		this(56000, xmlConfigPath);
	}

	/**
	 * Create the SpeechSpeaker object, bound to the specific port.
	 * 
	 * @param port
	 *            port the port number
	 */
	public SpeechSpeaker(int port, String xmlConfigPath) {
		this.initVariable(xmlConfigPath);
		linkLayer = new LinkLayer(port, false, this.Load_Engine_From_Config,
				this.Debug_Mode);
	}

	/**
	 * Start the Speech Speaker
	 * 
	 * @throws TimeoutException
	 *             if the engine have not send back the reply after the timeout
	 *             period. The timeout can be set in
	 *             {@link jspeech.config.SpeechSpeakerConfiguration} class
	 * @throws EngineNotFoundException
	 *             if the executable of the engine cannot be found
	 * @throws EngineConnectionException
	 *             if JSpeech fail to establish the connection with the engine
	 */
	public void start() throws EngineNotFoundException,
			EngineConnectionException {

		linkLayer.start(this.SDK_PATH, this.TIMEOUT);

		long startTime = System.currentTimeMillis();
		while ((System.currentTimeMillis() - startTime) < this.TIMEOUT) {
			try {
				if (linkLayer.readFromSocket().equalsIgnoreCase(
						"SPEECHVOICE:CONNECTED")) {
					return;
				} else {
					try {
						Thread.sleep(500);
					} catch (InterruptedException e) {
					}
				}
			} catch (RuntimeException re) {
				try {
					Thread.sleep(500);
				} catch (InterruptedException e) {
				}
			}
		}
		throw new TimeoutException();
	}

	/**
	 * Close the speech speaker
	 * 
	 * @throws IOException
	 *             if an I/O error occurs when closing speech speaker
	 */
	public void close() throws IOException {
		linkLayer.writeToSocket("CLOSE_ENGINE");
		linkLayer.close();
	}

	/**
	 * 
	 * @param text
	 *            Text that need to speak
	 * @throws TimeoutException
	 *             if the engine have not send back the reply after the timeout
	 *             period. The timeout can be set in
	 *             {@link jspeech.config.SpeechSpeakerConfiguration} class
	 */
	public void TTS(String text) {
		linkLayer.writeToSocket("TTS:" + text);

		receiveACK("FINISHED:TTS");
	}

	/**
	 * Restore all setting to default setting
	 * <li> Person Mary
	 * <li> Rate 0
	 * <li> Volume 100
	 * <p>
	 * &nbsp;
	 * <p>
	 * 
	 * @throws TimeoutException
	 *             if the engine have not send back the reply after the timeout
	 *             period. The timeout can be set in
	 *             {@link jspeech.config.SpeechSpeakerConfiguration} class
	 */
	public void restoreDefault() {
		this.setRate(0);
		this.setVoice(this.VOICE_MARY);
		this.setVolume(100);
	}

	/**
	 * <b>***** This method only use in Windows XP!!! Vista is not working due
	 * to the SAPI issue *****</b>
	 * <p>
	 * Set the person who are speaking
	 * 
	 * @param person
	 *            Speaking person. Access the variable by using
	 *            {@link jspeech.config.SpeechSpeakerConfiguration}
	 * @throws TimeoutException
	 *             if the engine have not send back the reply after the timeout
	 *             period. The timeout can be set in
	 *             {@link jspeech.config.SpeechSpeakerConfiguration} class
	 */
	public void setVoice(int person) {

		if (person == 1)
			linkLayer.writeToSocket("SET:VOICE:Mary");
		else if (person == 2)
			linkLayer.writeToSocket("SET:VOICE:Mike");
		else if (person == 3)
			linkLayer.writeToSocket("SET:VOICE:Sam");
		else if (person == 4)
			linkLayer.writeToSocket("SET:VOICE:Simplified Chinese");
		else
			return;

		receiveACK("FINISHED:SET:VOICE");
	}

	/**
	 * Set the speaking speed
	 * 
	 * @param rate
	 *            Speed of the speaking (from 0 to 10, 0 is default rate)
	 * @throws TimeoutException
	 *             if the engine have not send back the reply after the timeout
	 *             period. The timeout can be set in
	 *             {@link jspeech.config.SpeechSpeakerConfiguration} class
	 */
	public void setRate(int rate) {
		linkLayer.writeToSocket("SET:RATE:" + rate);
		receiveACK("FINISHED:SET:RATE");
	}

	/**
	 * Set the speaking volume
	 * 
	 * @param vol
	 *            Volume of the speaking (from 0 to 100, 100 is default rate)
	 * @throws TimeoutException
	 *             if the engine have not send back the reply after the timeout
	 *             period. The timeout can be set in
	 *             {@link jspeech.config.SpeechSpeakerConfiguration} class
	 */
	public void setVolume(int vol) {
		linkLayer.writeToSocket("SET:VOLUME:" + vol);
		receiveACK("FINISHED:SET:VOLUME");
	}

	/**
	 * Loop until the ACK is received. It ensure that the command is received by
	 * the engine
	 * 
	 * @param ack
	 *            The ACK that expected to receive
	 * @throws TimeoutException
	 *             if the engine have not send back the reply after the timeout
	 *             period. The timeout can be set in
	 *             {@link jspeech.config.SpeechSpeakerConfiguration} class
	 */
	private void receiveACK(String ack) throws TimeoutException {

		long start = System.currentTimeMillis();

		while (System.currentTimeMillis() - start < this.TIMEOUT) {

			String read = linkLayer.readFromSocket();

			if (read != null)
				if (read.equalsIgnoreCase(ack))
					return;
				else
					try {
						Thread.sleep(500);
					} catch (InterruptedException e) {

					}
		}
		throw new TimeoutException();
	}

	/**
	 * Init the configurature value from XML config file
	 * 
	 * @param xmlConfigPath
	 */
	private void initVariable(String xmlConfigPath)
			throws XMLConfigFileException {
		try {
			XMLReader Configuration = new XMLReader(xmlConfigPath);

			this.SDK_PATH = (String) Configuration
					.getConfig("JSpeech.SpeechSpeaker.SDKPath");

			this.TIMEOUT = (Integer) Configuration
					.getConfig("JSpeech.CommonConfiguration.Timeout");

			this.VOICE_ANNA = (Integer) Configuration
					.getConfig("JSpeech.SpeechSpeaker.VoiceAnna");

			this.VOICE_MARY = (Integer) Configuration
					.getConfig("JSpeech.SpeechSpeaker.VoiceMary");
			this.VOICE_MIKE = (Integer) Configuration
					.getConfig("JSpeech.SpeechSpeaker.VoiceMike");
			this.VOICE_SAM = (Integer) Configuration
					.getConfig("JSpeech.SpeechSpeaker.VoiceSam");
			this.VOICE_SimplifiedChinese = (Integer) Configuration
					.getConfig("JSpeech.SpeechSpeaker.VoiceSimplifiedChinese");

			this.Load_Engine_From_Config = (Boolean) Configuration
					.getConfig("JSpeech.CommonConfiguration.LoadEngineFromConfig");

			this.Debug_Mode = (Boolean) Configuration
					.getConfig("JSpeech.CommonConfiguration.DebugMode");

		} catch (RuntimeException e) {
			throw new XMLConfigFileException(xmlConfigPath, e);
		}
	}
}
